package com.gxtbindings.bindings.client;

import java.util.ArrayList;
import java.util.List;

import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.Record;
import com.extjs.gxt.ui.client.store.StoreEvent;
import com.extjs.gxt.ui.client.store.StoreListener;
import com.gxtbindings.bindings.client.util.EventList;
import com.gxtbindings.bindings.client.util.EventListListener;
import com.gxtbindings.bindings.client.util.ListChangeEvent;
import com.gxtbindings.bindings.client.util.ListChangeEvent.ListChangeType;

/**
 * Allows for two-way binding between an EventList (impl of java.uti.List) and a GXT ListStore.
 * 
 * Example usage:
 * EditorGrid<BeanModel> grid = new EditorGrid<BeanModel>(new ListStore<BeanModel>(), columnModel);
 * EventList<Person> list = new EventList<Person>();
 * ListStoreBinding<Person,BeanModel> binding = new ListStoreBinding<Person, BeanModel>(grid.getStore());
 * binding.setConverter(new BeanModelConverter<Person>());
 * binding.bind(list);
 * 
 * Now, whenever the list is manipluated, the store will be updated, and vice versa.  Note: If you update
 * a field on a bean directly, you still must find the model in the store and call store.update(model).  If
 * the store contains beanmodels, you can use BeanModelStoreUtil.findModel to find the appropriate BeanModel.
 * 
 * @author Jonathan Janisch
 *
 * @param <E>	the type of object contained in the EventList 
 * @param <M>	the type of ModelData obj contained in the ListStore
 */
public class ListStoreBinding<E,M extends ModelData> implements Bindable {

	private ListStore<M> store;

	private EventList<E> list;

	private EventListListener<E> changeListener;

	private StoreListener<M> storeListener;

	private ListStoreConverter<E, M> converter;

	private boolean ignoreEvent = false;
	
	private List<E> originalList = new ArrayList<E>();

	/**
	 * Creates a new binding backed by the given store
	 * 
	 * @param store	the liststore
	 */
	public ListStoreBinding(ListStore<M> store) {
		this.store = store;

		changeListener = new EventListListener<E>() {
			public void listChanged(ListChangeEvent<E> ev) {
				onListChange(ev);
			}
		};

		storeListener = new StoreListener<M>() {

			@Override
			public void storeAdd(StoreEvent<M> se) {
				onStoreAdd(se);
			}

			@Override
			public void storeClear(StoreEvent<M> se) {
				onStoreClear();
			}

			@Override
			public void storeDataChanged(StoreEvent<M> se) {
			}

			@Override
			public void storeRemove(StoreEvent<M> se) {
				onStoreRemove(se);
			}
		};
	}

	/**
	 * Returns the converter used to convert from obj to modeldata and vice versa.
	 * 
	 * @return the converter used to convert from obj to modeldata and vice versa.
	 */
	public ListStoreConverter<E,M> getConverter() {
		return converter;
	}

	/**
	 * Sets the converter used to convert from obj to modeldata and vice versa.
	 * 
	 * @param converter	the converter to use
	 */
	public void setConverter(ListStoreConverter<E,M> converter) {
		this.converter = converter;
	}

	/**
	 * When items are added to the store, syncs the EventList
	 * @param se
	 */
	protected void onStoreAdd(StoreEvent<M> se) {
		if (!ignoreEvent) {
			ignoreEvent = true;

			if (converter != null) {
				
				List<M> models = se.getModels();
				
				if (se.getModel() != null)
				{
					models = new ArrayList<M>();
					models.add(se.getModel());
				}
				
				int index = se.getIndex();
				for (M model : models) {
					
					E obj = converter.convertModelValue(model);
					list.add(index, obj);
					index++;
				}
			}

			ignoreEvent = false;
		}
	}
	
	
	protected void onStoreRemove(StoreEvent<M> se) {
		if (!ignoreEvent) {
			ignoreEvent = true;

			List<M> models = se.getModels();
			
			if (se.getModel() != null)
			{
				models = new ArrayList<M>();
				models.add(se.getModel());
			}
			if (converter != null) {
				for (M model : models) {
					E obj = converter.convertModelValue(model);
					list.remove(obj);
				}
			}

			ignoreEvent = false;
		}
	}
	
	protected void onStoreClear() {
		if (!ignoreEvent) {
			ignoreEvent = true;
			list.clear();
			ignoreEvent = false;
		}
	}

	/**
	 * Handles list change events and delegate to methods
	 * @param le
	 */
	protected void onListChange(ListChangeEvent<E> le) {
		if (!ignoreEvent) {
			ignoreEvent = true;

			switch (le.getType())
			{
			case INSERT:
				onListObjectsAdded(le);
				break;
			case DELETE:
				onListObjectsRemoved(le);
				break;
			case UPDATE:
				onListObjectsUpdated(le);
				break;
			}
			ignoreEvent = false;
		}
	}

	private void onListObjectsUpdated(ListChangeEvent<E> le) {
		if (converter != null) {
			for (int i = le.getStartIndex(); i < le.getEndIndex(); i++)
			{
				// does not mean the contents of obj changed, only the object at index changed
				// store doesn't have a store.set so we must remove and insert it
				store.remove(store.getAt(i));
				//store.remove(i);
				
				E obj = le.getSourceList().get(i);
				store.insert(converter.convertObjectToModel(obj), i);
			}
		}
	}


	private void onListObjectsAdded(ListChangeEvent<E> le) {
		if (converter != null) {
			
			int index = le.getStartIndex();
			for (E obj : le.getSourceList().subList(le.getStartIndex(), le.getEndIndex())) {
				store.insert(converter.convertObjectToModel(obj), index);
				index++;
			}
		}
	}

	protected void onListObjectsRemoved(ListChangeEvent<E> le) {
		// Maybe more efficient / fire clear event
		if (le.getSourceList().size() == 0 && le.getEndIndex() - le.getStartIndex() > 1)
		{
			store.removeAll();
		}
		else
		{
			for (int i = le.getStartIndex(); i < le.getEndIndex(); i++)
			{
				store.remove(store.getAt(i));
				//store.remove(i);
			}
		}
	}
	
	/**
	 * Commits the changes to the store.  You must use this instead of calling store.commitChanges()
	 */
	public void commitChanges()
	{
		originalList.clear();
		originalList.addAll(list);
		store.commitChanges();
	}
	
	/**
	 * Rejects changes to the store and thus reverts the EventList back to it's original state.
	 */
	public void rejectChanges()
	{
		// store.rejectChanges must come before the list is cleared.  Clearing the list will call store.removeAll which
		// clears the modified records from the store.
		store.rejectChanges();
		list.clear();
		list.addAll(originalList);
	}
	
	/**
	 * Returns objects that were added to the list or store since last commit.
	 * 
	 * @return objects that were added to the list or store since last commit.
	 */
	public List<E> getNewObjects()
	{
		List<E> added = new ArrayList<E>();
		for (E obj : list)
		{
			if (!originalList.contains(obj))
			{
				added.add(obj);
			}
		}
		return added;
	}
	
	/**
	 * Returns objects that were removed from the list or store since last commit.
	 * 
	 * @return objects that were removed from the list or store since last commit.
	 */
	public List<E> getRemovedObjects()
	{
		List<E> removed = new ArrayList<E>();
		for (E obj : originalList)
		{
			if (!list.contains(obj))
			{
				removed.add(obj);
			}
		}
		return removed;
	}
	
	/**
	 * Returns objects that were modified in the store (not including new modeldata) since last commit.  Note:
	 * If you modify a bean directly, you must still call store.update(model) in order to register the change.
	 * 
	 * @return objects that were modified since the last commit.
	 */
	@SuppressWarnings("unchecked")
	public List<E> getModifiedObjects()
	{
		List<E> modified = new ArrayList<E>();
		
		List<Record> records = store.getModifiedRecords();
		
		for (Record r : records)
		{
			E obj = converter.convertModelValue((M)r.getModel());
			modified.add(obj);
		}
		
		modified.removeAll(getNewObjects());
		return modified;
	}
	
	/**
	 * Binds the given event list with the store.
	 */
	public void bind(EventList<E> modelList) {
		unbind();
		
		this.list = modelList;
		store.removeAll();
		
		store.addStoreListener(storeListener);
		list.addEventListListener(changeListener);

		onListChange(new ListChangeEvent<E>(ListChangeType.INSERT, 0, modelList.size(), list));
		
		commitChanges();
	}
	
	/**
	 * Unbinds the event list from the store.
	 */
	public void unbind()
	{
		if (list != null && store != null)
		{
			store.removeStoreListener(storeListener);
			list.removeEventListListener(changeListener);
			this.list = null;
			this.originalList.clear();
		}
	}

	@Override
	public boolean hasChanges() {
		return getNewObjects().size() > 0 || getModifiedObjects().size() > 0 || getRemovedObjects().size() > 0;
	}
}
